﻿namespace NTLib.Pipe.Providers
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using NTLib.Pipe.Targets;
    using NTLib.Core.Pool;
    using NTLib.Pipe.Containers;
    using System.Threading;
    using NTLib.Core.Tasks;
    using NTLib.Core.Threading;
    using NTLib.Pipe.Exceptions;
    using NTLib.Pipe.Descriptors;


    /// <summary>
    /// Provide pipe to communicate localy throught the current AppDomain
    /// </summary>
    public abstract class BasePipeProvider : IPipeProvider
    {
        #region Fields

        protected readonly TypePool _type;
        protected readonly AsyncLock _lock;
        protected readonly Dictionary<Guid, IPipe> _openPipes;

        #endregion

        #region Ctor

        /// <summary>
        /// Prevents a default instance of the <see cref="BasePipeProvider" /> class from being created.
        /// </summary>
        protected BasePipeProvider()
        {
            this._openPipes = new Dictionary<Guid, IPipe>();
            this._type = new TypePool();
            this._lock = new AsyncLock();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the identifier.
        /// </summary>
        public abstract Guid Id { get; }


        #endregion

        #region Methods

        /// <summary>
        /// Determines whether this instance can get the pipe refert by the specified identifier.
        /// </summary>
        /// <param name="Id">The identifier.</param>
        /// <param name="token"></param>
        /// <returns>
        ///   <c>true</c> if the pipe can be get by this provider.
        /// </returns>
        public virtual async Task<bool> CanGetPipeAsync(Guid Id, CancellationToken token = default(CancellationToken))
        {
            using (var lck = await this._lock.LockAsync(token))
            {
                token.ThrowIfCancellationRequested();
                return this._openPipes.ContainsKey(Id);
            }
        }

        /// <summary>
        /// Determines whether this instance can managed target the specified target.
        /// </summary>
        /// <param name="desc">Target description used to define the target.</param>
        /// <param name="token"></param>
        /// <returns>
        ///   <c>true</c> if the provider can managed the specific target.
        /// </returns>
        public abstract Task<bool> CanManagedTargetAsync(ITargetDescriptor desc, CancellationToken token = default(CancellationToken));

        /// <summary>
        /// Creates asynchronously the pipe in function of the target.
        /// </summary>
        /// <typeparam name="T">Type of the object that will travel through the pipe.</typeparam>
        /// <param name="type"></param>
        /// <param name="target">The target description used to construct properly the pipe.</param>
        /// <param name="token"></param>
        /// <returns>
        /// Pipe object used to transfert data.
        /// </returns>
        public virtual async Task<IPipeManipulatorOwner<T>> CreateAsync<T>(PipeType type, ITargetDescriptor target, CancellationToken token = default(CancellationToken))
        {
            IPipe<T> pipeWithsource = null;
            BasePipeRelay<T> relay = null;

            try
            {

                if (type == PipeType.OneWay)
                    relay = this._type.Get<PushOnlyPipeOwner<T>>();
                else if (type == PipeType.FromTarget)
                    relay = this._type.Get<ReadOnlyPipeOwner<T>>();
                else
                    relay = this._type.Get<TwoWayPipeOwner<T>>();

                token.ThrowIfCancellationRequested();

                pipeWithsource = await OnCreateSourceAsync<T>(type, target, token);

                if (pipeWithsource == null)
                    throw new NoPipeSourceAvailableException();

                token.ThrowIfCancellationRequested();

                token.ThrowIfCancellationRequested();

                pipeWithsource.StatusChanged -= PipeWithsource_StatusChanged;
                pipeWithsource.StatusChanged += PipeWithsource_StatusChanged;

                using (var lck = await this._lock.LockAsync(token))
                {
                    token.ThrowIfCancellationRequested();
                    this._openPipes.Add(pipeWithsource.Id, pipeWithsource);
                }

                token.ThrowIfCancellationRequested();

                relay.SetPipe(pipeWithsource);
                return (IPipeManipulatorOwner<T>)relay;
            }
            catch (OperationCanceledException)
            {
                if (pipeWithsource != null)
                {
                    bool dispose = false;
                    using (var lck = await this._lock.LockAsync())
                    {
                        if (this._openPipes.ContainsKey(pipeWithsource.Id))
                        {
                            this._openPipes.Remove(pipeWithsource.Id);
                            dispose = true;
                        }
                    }

                    if (dispose)
                        pipeWithsource.Dispose();
                }

                if (relay != null)
                    relay.Dispose();

                throw;
            }
        }

        /// <summary>
        /// Gets the other side of the pipe from the pipe ID.
        /// </summary>
        /// <typeparam name="T">Type object that will travel through the pipe</typeparam>
        /// <param name="pipeId">The pipe identifier.</param>
        /// <param name="token"></param>
        /// <returns>
        /// Return asynchrounously the other side of the pipe from the pipe ID
        /// </returns>
        /// <exception cref="NTLib.Pipe.Exceptions.NoPipeFoundException"></exception>
        public virtual async Task<IPipeManipulator<T>> GetPipeAsync<T>(Guid pipeId, CancellationToken token = default(CancellationToken))
        {
            IPipeManipulator<T> pipe = null;
            try
            {
                using (var lck = await this._lock.LockAsync(token))
                {
                    token.ThrowIfCancellationRequested();
                    IPipe pipeSource = null;

                    if (this._openPipes.TryGetValue(pipeId, out pipeSource))
                    {
                        if (pipeSource.PipeType == PipeType.OneWay)
                            pipe = this._type.Get<ReadOnlyPipe<T>>();
                        else if (pipeSource.PipeType == PipeType.FromTarget)
                            pipe = this._type.Get<PushOnlyPipe<T>>();
                        else
                            pipe = this._type.Get<TwoWayPipe<T>>();

                        ((BasePipeRelay<T>)pipe).SetPipe((IPipe<T>)pipeSource);

                        return pipe;
                    }
                }
                throw new NoPipeFoundException();
            }
            catch (OperationCanceledException)
            {
                if (pipe != null)
                    pipe.Dispose();
                throw;
            }
        }

        /// <summary>
        /// Release the item from the storage
        /// </summary>
        /// <param name="obj">The object.</param>
        private void PipeWithsource_StatusChanged(object sender, Core.Args.ValueChangedEventArgs<PipeStatus> e)
        {
            if (e.NewValue == PipeStatus.Closed)
            {
                IPipe pipe = (IPipe)sender;

                pipe.StatusChanged -= PipeWithsource_StatusChanged;
                using (var lck = this._lock.LockAsync().WaitResult())
                {
                    if (this._openPipes.ContainsKey(pipe.Id))
                    {
                        this._openPipes.Remove(pipe.Id);
                    }
                }
            }
        }

        /// <summary>
        /// Called to create the specific pipe source associate the target.
        /// </summary>
        /// <param name="type">The type of the pipe.</param>
        /// <param name="target">The target of the pipe.</param>
        /// <param name="token">The cancel token.</param>
        /// <returns>IPipeSource that could managed object transfert in both ways.</returns>
        protected abstract Task<IPipe<T>> OnCreateSourceAsync<T>(PipeType type, ITargetDescriptor target, CancellationToken token);

        #endregion
    }
}
